/* ipp-usb - HTTP reverse proxy, backed by IPP-over-USB connection to device
 *
 * Copyright (C) 2020 and up by Alexander Pevzner (pzz@apevzner.com)
 * See LICENSE for license terms and conditions
 *
 * Device object brings all parts together
 */

package ippusb

import (
	"context"
	"net"
	"net/http"
	"time"
)

// Device object brings all parts together, namely:
//   - HTTP proxy server
//   - USB-backed http.Transport
//   - DNS-SD advertiser
//
// There is one instance of Device object per USB device
type Device struct {
	UsbAddr      UsbAddr       // Device's USB address
	State        *DevState     // Persistent state
	HTTPClient   *http.Client  // HTTP client for internal queries
	HTTPProxy    *HTTPProxy    // HTTP proxy
	UsbTransport *UsbTransport // Backing USB transport
	Log          *Logger       // Device's logger
}

// NewDevice creates new Device object
func NewDevice(desc UsbDeviceDesc) (*Device, error) {
	dev := &Device{
		UsbAddr: desc.UsbAddr,
	}

	var err error
	var info UsbDeviceInfo
	var listener net.Listener
	var _ *IppPrinterInfo
	var log *LogMessage

	// Create USB transport
	dev.UsbTransport, err = NewUsbTransport(desc)
	if err != nil {
		goto ERROR
	}

	// Obtain device's logger
	info = dev.UsbTransport.UsbDeviceInfo()
	dev.Log = dev.UsbTransport.Log()

	// Load persistent state
	dev.State = LoadDevState(info.Ident(), info.Comment())

	// Create HTTP client for local queries
	dev.HTTPClient = &http.Client{
		Transport: dev.UsbTransport,
	}

	// Create net.Listener
	listener, err = dev.State.HTTPListen()
	if err != nil {
		goto ERROR
	}

	// Create HTTP server
	dev.UsbTransport.SetDeadline(time.Now().Add(DevInitTimeout))
	dev.HTTPProxy = NewHTTPProxy(dev.Log, listener, dev.UsbTransport)

	// Obtain DNS-SD info for IPP
	log = dev.Log.Begin()
	defer log.Commit()

	_, err = IppService(log,
		dev.State.HTTPPort, info, dev.UsbTransport.Quirks(),
		dev.HTTPClient)

	if err != nil {
		dev.Log.Error('!', "IPP: %s", err)
	}

	log.Flush()

	if dev.UsbTransport.DeadlineExpired() {
		err = ErrInitTimedOut
		goto ERROR
	}

	if err != nil {
		dev.Log.Error('!', "ESCL: %s", err)
	}

	log.Flush()

	if dev.UsbTransport.DeadlineExpired() {
		err = ErrInitTimedOut
		goto ERROR
	}

	// Enable handling incoming requests
	dev.UsbTransport.SetDeadline(time.Time{})
	dev.HTTPProxy.Enable()

	return dev, nil

ERROR:
	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
	}

	if dev.UsbTransport != nil {
		dev.UsbTransport.Close(true)
	}

	if listener != nil {
		listener.Close()
	}

	return nil, err
}

// Shutdown gracefully shuts down the device. If provided context
// expires before the shutdown is complete, Shutdown returns the
// context's error
func (dev *Device) Shutdown(ctx context.Context) error {

	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
		dev.HTTPProxy = nil
	}

	if dev.UsbTransport != nil {
		return dev.UsbTransport.Shutdown(ctx)
	}

	return nil
}

// Close the Device
func (dev *Device) Close() {
	if dev.HTTPProxy != nil {
		dev.HTTPProxy.Close()
		dev.HTTPProxy = nil
	}

	if dev.UsbTransport != nil {
		dev.UsbTransport.Close(false)
		dev.UsbTransport = nil
	}
}
